using System.Collections.Generic;
using System.Linq;
using LoggingMagic.Extensions;
using LoggingMagic.LoggingParameters;
using Mono.Cecil;

namespace LoggingMagic.AssemlyOperations
{
    internal class MethodFinder
    {

        public struct MethodInfo
        {
            public MethodDefinition MethodDefinition;
            public CustomAttribute Attribute;
        }

        private struct PropertyInfo
        {
            public PropertyDefinition PropertyDefinition;
            public CustomAttribute Attribute;
        }

        private readonly ModuleDefinition _module;
        private readonly AttributesFinder _attributesFinder;
        private readonly IMethodFinderParameters _parameters;

        public MethodFinder(ModuleDefinition module, AttributesFinder attributesFinder, IMethodFinderParameters parameters)
        {
            _module = module;
            _attributesFinder = attributesFinder;
            _parameters = parameters;
        }

        public IEnumerable<MethodInfo> FindMethods()
        {
            if (!_attributesFinder.FindAttributes(_parameters.LogAttributeName, _parameters.NoLogAttributeName))
                return null;

            var assemblyLevel = _module.Assembly.CustomAttributes.FirstAttributeFrom(_attributesFinder.Magic);

            var types = _module.Types.Where(x => x.IsClass).Select(x => new { Type = x, ClassLevel = FindImplicit(x, assemblyLevel) });

            // making a list of properties to process
            var methods = from type in types
                          from method in type.Type.Methods
                          let directAttribute = method.CustomAttributes.FirstAttributeFrom(_attributesFinder.Magic)
                          let magicAttribute = directAttribute ?? type.ClassLevel
                          // must not have NoMagic attribute applied
                          where
                              !method.IsAbstract &&
                              !method.CustomAttributes.ContainsAttributeFrom(_attributesFinder.NoMagic) &&
                              !method.IsGetter && !method.IsSetter
                          // must have assembly, class or property level Magic attribute applied
                          where magicAttribute != null
                          // select task related data
                          select new MethodInfo { MethodDefinition = method, Attribute = magicAttribute };
            var methodList = methods.ToList();

            var properties = from type in types
                             from property in type.Type.Properties
                             let directAttribute = property.CustomAttributes.FirstAttributeFrom(_attributesFinder.Magic)
                             let magicAttribute = directAttribute ?? type.ClassLevel
                             // must not have NoMagic attribute applied
                             where
                                 !property.CustomAttributes.ContainsAttributeFrom(_attributesFinder.NoMagic)
                             // must have assembly, class or property level Magic attribute applied
                             where magicAttribute != null
                             // select task related data
                             select new PropertyInfo { PropertyDefinition = property, Attribute = magicAttribute };

            if (_parameters.LogGetters)
            {
                methodList.AddRange(properties.Where(x => x.PropertyDefinition.GetMethod != null && !x.PropertyDefinition.GetMethod.IsAbstract).Select(x => new MethodInfo
                                                               {
                                                                   Attribute = x.Attribute,
                                                                   MethodDefinition = x.PropertyDefinition.GetMethod
                                                               }));
            }
            if (_parameters.LogSetters)
            {
                methodList.AddRange(properties.Where(x => x.PropertyDefinition.SetMethod != null && !x.PropertyDefinition.SetMethod.IsAbstract).Select(x => new MethodInfo
                                                    {
                                                        Attribute = x.Attribute,
                                                        MethodDefinition = x.PropertyDefinition.SetMethod
                                                    }));
            }


            return methodList;
        }

        /// <summary>
        /// We traverse class hierarchy upwards and look for Magic/NoMagic attribute.
        /// If Magic attribute found first, class transformation is implicit.
        /// Otherwise explicit.
        /// </summary>
        CustomAttribute FindImplicit(TypeDefinition type, CustomAttribute @default)
        {
            var x = (from t in GetClassHierarchy(type)
                     from a in t.CustomAttributes
                     let at = a.AttributeType.FullName
                     where _attributesFinder.Magic.Contains(at) || _attributesFinder.NoMagic.Contains(at)
                     select new
                     {
                         Magic = _attributesFinder.Magic.Contains(at) ? a : null
                     }).FirstOrDefault();

            return x == null ? @default : x.Magic;
        }

        static IEnumerable<TypeDefinition> GetClassHierarchy(TypeDefinition type, bool includeSystem = false)
        {
            for (var scan = type; scan != null; scan = Resolve(scan.BaseType, includeSystem))
                yield return scan;
        }
        static TypeDefinition Resolve(TypeReference reference, bool includeSystem = true)
        {
            if (!includeSystem && reference != null && reference.FullName.StartsWith("System."))
                return null;

            return reference != null ? reference.Resolve() : null;
        }
    }
}